package se.lnu.axez.tools;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

import se.lnu.axez.TriMesh;

final class ObjLoader {
	private final StringBuffer vertexLines = new StringBuffer();
	private final StringBuffer normalLines = new StringBuffer();
	private final StringBuffer uvLines = new StringBuffer();
	private final StringBuffer facesLines = new StringBuffer();
	
	private int verticesCount = 0;
	private int normalsCount = 0;
	private int uvCount = 0;
	private int facesCount = 0;
	
	private float[] vertices;
	private float[] normals;
	private float[] uv;
	private float[] faceVertices;
	private float[] faceNormals;
	private float[] faceUV;
	
	int getFacesCount() {
		return facesCount;
	}

	float[] getFaceUV() {
		return faceUV;
	}

	float[] getFaceNormals() {
		return faceNormals;
	}

	float[] getFaceVertices() {
		return faceVertices;
	}

	public void loadArrays(InputStream objStream) {
		this.readStreamData(objStream);
		this.loadVertices();
		this.loadNormals();
		this.loadUV();
		this.loadFaces();
	}
	
	private void readStreamData(InputStream objStream) {
		BufferedReader reader = new BufferedReader(new InputStreamReader(objStream), 1024 * 8);
		try {
			objStream.read();
			String line = reader.readLine();
			while (line != null) {
				line = line.trim();
				
				if (line.startsWith("v ")) {
					this.verticesCount++;
					this.vertexLines.append(line.substring(1).trim()).append("\n");
				} else if (line.startsWith("vn ")) {
					this.normalsCount++;
					this.normalLines.append(line.substring(2).trim()).append("\n");
				} else if (line.startsWith("vt ")) {
					this.uvCount++;
					this.uvLines.append(line.substring(2).trim()).append("\n");
				} else if (line.startsWith("f ")) {
					this.facesCount++;
					this.facesLines.append(line.substring(1).trim()).append("\n");
				}
				
				line = reader.readLine();
			}
		} catch (Exception ex) {
//			System.err.println(ex.getMessage());
			ex.printStackTrace();
			throw new RuntimeException();
		}

		this.vertices = new float[this.verticesCount * TriMesh.VERTEX_COORDINATES];
		this.faceVertices = new float[this.facesCount * 3 * TriMesh.VERTEX_COORDINATES];
		
		if (this.normalsCount > 0) {
			this.normals = new float[this.normalsCount * TriMesh.VERTEX_COORDINATES];
			this.faceNormals = new float[this.facesCount * 3 * TriMesh.VERTEX_COORDINATES];
		} else {
			this.normals = this.faceNormals = null;
		}

		if (this.uvCount > 0) {
			this.uv = new float[this.uvCount * TriMesh.UV_COMPONENTS];
			this.faceUV = new float[this.facesCount * 3 * TriMesh.UV_COMPONENTS];
		} else {
			this.uv = this.faceUV = null;
		}
	}
	
	private void loadVertices() {
		String[] lines = this.vertexLines.toString().split("\n");
		loadArray(this.vertices, lines, TriMesh.VERTEX_COORDINATES, true);
	}
	
	private void loadNormals() {
		if (this.normals == null) {
			return;
		}
		
		String[] lines = this.normalLines.toString().split("\n");
		loadArray(this.normals, lines, TriMesh.VERTEX_COORDINATES, false);
	}
	
	private void loadUV() {
		if (this.uv == null) {
			return;
		}

		String[] lines = this.uvLines.toString().split("\n");
		loadArray(this.uv, lines, TriMesh.UV_COMPONENTS, false);
	}
	
	private void loadFaces(){
		String[] lines = this.facesLines.toString().split("\n");
		
		int faceIndex = 0, faceUvIndex = 0;
		for (int i = 0; i < lines.length; i++) {
			// each line describe one face
			// first get 3 parts corresponding to face vertices
			String[] lineParts = lines[i].split("\\s+");
			
			if (lineParts.length != 3) {
				throw new RuntimeException("Loader supports only triangular faces.");
			}
			
			// process each face vertex:
			// collect vertex index, 
			// texture coordinates index (if defined) 
			// and normal index (if defined) 
			for (int j = 0; j < lineParts.length; j++) {
				String[] sequence = lineParts[j].split("/");
				// vertex index
				int vertexIndex = Integer.parseInt(sequence[0]) - 1 ;
				this.faceVertices[faceIndex] = this.vertices[vertexIndex * TriMesh.VERTEX_COORDINATES];
				this.faceVertices[faceIndex + 1] = this.vertices[vertexIndex * TriMesh.VERTEX_COORDINATES + 1];
				this.faceVertices[faceIndex + 2] = this.vertices[vertexIndex * TriMesh.VERTEX_COORDINATES + 2];
				
				if (sequence.length > 1 && sequence[1].length() > 0) {
					// if we have uv index assigned to the face
					int uvIndex = Integer.parseInt(sequence[1]) - 1;
					this.faceUV[faceUvIndex] = this.uv[uvIndex * TriMesh.UV_COMPONENTS];
					this.faceUV[faceUvIndex + 1] = this.uv[uvIndex * TriMesh.UV_COMPONENTS + 1];
				}
				
				if (sequence.length > 2 && sequence[2].length() > 0) {
					// if we have normal index assigned to the face
					int normalIndex = Integer.parseInt(sequence[2]) - 1;
					this.faceNormals[faceIndex] = this.normals[normalIndex * TriMesh.VERTEX_COORDINATES];
					this.faceNormals[faceIndex + 1] = this.normals[normalIndex * TriMesh.VERTEX_COORDINATES + 1];
					this.faceNormals[faceIndex + 2] = this.normals[normalIndex * TriMesh.VERTEX_COORDINATES + 2];
				}
				
				faceIndex += TriMesh.VERTEX_COORDINATES;
				faceUvIndex += TriMesh.UV_COMPONENTS;
			}
		}
	}
	
	private static void loadArray(float[] array, String[] lines, int componentsCount, boolean normalize) {
		int index = 0;
		
		// following code structure used intentionally to reduce number of if checks inside the loop
		if (!normalize) {
			// if no need to normalize - simply parse the array
			for (int i = 0; i < lines.length; i++) {
				fillFloatValues(array, lines[i], index, componentsCount);
				index += componentsCount;
			}
			
			return;
		} else {
			// if we need to normalize, first find maximal value in the array
			float max = 0;
			for (int i = 0; i < lines.length; i++) {
				max = fillFloatValuesGetMax(array, lines[i], index, componentsCount, max);
				index += componentsCount;
			}
			
			// second - normalize
			for (int i = 0; i < array.length; i++) {
				array[i] /= max;
			}
		}
	}
	
	private static float fillFloatValuesGetMax(float[] array, String line, int startIndex, int count, float oldMax) {
		String[] lineParts = line.split("\\s+");
		if (lineParts.length < count) {
			throw new RuntimeException(
					String.format("Expected number of components %d, but was %d.", 
							count, 
							lineParts.length));
		}
		
		float max = oldMax;
		for (int i = 0; i < count; i++) {
			float val = Float.parseFloat(lineParts[i]);
			max = Math.max(max, Math.abs(val));
			array[startIndex + i] = val;
		}
		
		return max;
	}
	
	private static void fillFloatValues(float[] array, String line, int startIndex, int count) {
		String[] lineParts = line.split("\\s+");
		if (lineParts.length < count) {
			throw new RuntimeException(
					String.format("Expected number of components %d, but was %d.", 
					count, 
					lineParts.length));
		}
		
		for (int i = 0; i < count; i++) {
			array[startIndex + i] = Float.parseFloat(lineParts[i]);
		}
	}
}

















